package com.excilys.jnullsafe;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;

/**
 * TODO handle access modifiers ?
 * 
 * 
 * 
 * Handling arrays is way too complicated (we cannot mock arrays => must return
 * arrays of mocked objects. What should we do when returned null ? What should
 * we do with a double dimension array ?
 * 
 * Instead, we just stop being safe when arrays are at stake
 */
public class NullSafeHandler implements InvocationHandler {

    private final Object wrappedInstance;
    private final Type   parameterizedType;

    protected NullSafeHandler(Object wrappedInstance, Type parameterizedType) {
        this.wrappedInstance = wrappedInstance;
        this.parameterizedType = parameterizedType;
    }

    public Object getWrappedInstance() {
        return wrappedInstance;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Object rawResult = isInstanceNull() ? null : callMethod(method, args);

        ReturnType returnType = new ReturnType(method);

        Object result;
        if (returnType.isArray()) {
            result = rawResult;
        } else if (returnType.isPrimitive()) {
            if (isInstanceNull()) {
                throw new UnsupportedOperationException("Instance is null, cannot wrap a primitive nor return null");
            }
            result = rawResult;
        } else {
            if (returnType.isGeneric()) {
                returnType.extractTypeArgument(parameterizedType);
            }
            if (returnType.isVoid()) {
                result = null;
            } else {
                result = wrapResult(rawResult, returnType);
            }
        }

        return result;
    }

    public boolean isInstanceNull() {
        return wrappedInstance == null;
    }

    private Object callMethod(Method method, Object[] args) throws Throwable {

        boolean accessible = method.isAccessible();
        if (!accessible) {
            method.setAccessible(true);
        }

        try {
            return method.invoke(wrappedInstance, args);
        } catch (InvocationTargetException e) {
            throw e.getCause();
        } finally {
            if (!accessible) {
                method.setAccessible(false);
            }
        }
    }

    private Object wrapResult(Object rawResult, ReturnType returnType) {
        return JNullSafe.wrap(rawResult, returnType.asType());
    }

}